/*
 * sysmo 服务器主控模块
 * Copyright (C) 2009  YuLicheng
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.sysmo.server;

import java.io.File;
import java.net.Socket;
import java.sql.SQLException;
import java.util.HashMap;
import org.sysmo.server.user.*;
import org.sysmo.server.node.*;
import java.util.List;
import java.util.Vector;
import org.sysmo.communication.CommunicationInterface;
import org.sysmo.communication.CommunicationProtocol;
import org.sysmo.db.DatabaseInterface;
import org.sysmo.db.DbClient;
import org.sysmo.db.DbItem;
import org.sysmo.db.DbWarning;
import org.sysmo.server.node.item.ConnectedItem;
import org.sysmo.server.node.item.Item;
import org.sysmo.server.node.item.ItemNotExistsException;
import org.sysmo.server.node.item.Warning;
import org.sysmo.server.node.item.WarningNotExistsException;
import org.sysmo.server.node.item.graphgroup.GraphGroup;
import org.sysmo.server.node.item.graphgroup.GraphGroupNotExistsException;
import org.sysmo.server.node.item.graphgroup.value.Value;
import org.sysmo.server.node.item.graphgroup.value.ValueNotExistsException;
import org.sysmo.server.util.ServerSettings;
import org.w3c.dom.Document;

/**
 *
 * sysmo 服务器的主控模块，sysmo 服务器从这里启动。
 * <br />
 * 此模块整合整个sysmo系统的其他部分，包括监听客户端连接、提供Web用户数据读取接口以
 * 及客户端的远程部署配置等。
 * <br />
 *
 * 在Java EE应用服务器部署sysmo时，应在应用服务器启动时同时实例化此模块。可用一个
 * Servlet，在其web.xml配置中此Servlet的下增加属性
 * <code>&lt;load-on-startup&gt;1&lt;/load-on-startup&gt;</code>，然后在此
 * Servlet的代码中重载init方法，并在此方法中创建一个新线程，并且在新线程中实例化本
 * 模块。
 * <br />
 *
 * 由于sysmo服务器本身可独立于应用服务器运行，而数据库访问通常可交由应用服务器或使用
 * 应用服务器的框架托管，因此实例化此模块时，需要给构造方法传入指定的数据库接口实现。
 * <br />
 * 
 * 另外还需要给出服务器的配置文件名，使得服务器能够按照配置执行初始化。
 * <br />
 *
 * 在实例化此模块后，可以立即调用{@link #doServerLoop() }方法将控制权交给sysmo 服
 * 务器。
 * <br />
 *
 * 注意：对于被监控机器列表，可从此类中获取，后续的信息（如机器上的监控项）则使用获
 * 取的机器实例中的相应方法获取。某些其他元数据配置的获取和更改直接通过相应的
 * <code>org.sysmo.server.config</code>包内的对象进行操作。
 * <br />
 *
 * @author YuLicheng
 * @see #SysmoServer(String)
 * @see DatabaseInterface
 */
public class SysmoServer implements NodeStateChangeListener {

    private DatabaseInterface dbInterface;
    private Vector<NewNode> newNodes = new Vector<NewNode>();
    private HashMap<Integer, AddedNode> addedNodes = new HashMap<Integer, AddedNode>();
    private boolean isRunning;
    private Thread listeningThread;
    private String configFile;

    /**
     * 构造方法中包含了sysmo 服务器启动时需要执行的一些初始化动作，主要包括服务器配
     * 置文件的读取，设置各参数项等操作。
     * <br />
     *
     * 构造方法调用后即服务器启动完成。之后必须调用{@link #doServerLoop()}方法将
     * 控制权交给sysmo 服务器，这样才能使得服务器能够监听和处理各客户端连接。
     * 
     * @param configFileName 配置文件文件名。
     * @throws Exception 配置文件读取错误时抛出异常。
     * @see #doServerLoop()
     * @see DatabaseInterface
     */
    public SysmoServer(String configFileName)
            throws Exception {
        try {
            new ServerSettings(configFileName);
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
        this.configFile = configFileName;
        //TODO: 初始化数据库。
        dbInterface = new DatabaseInterface(ServerSettings.settings.settingMap.get("/db/jdbc-pool"));
        Vector<DbClient> clients = dbInterface.query_ClientTable_all();
        for (DbClient client : clients) {
            try {
                addedNodes.put(client.cid,
                        new UnconnectedNode(this, dbInterface, client.cid));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 此方法是sysmo 服务器的控制主循环。
     * <br />
     *
     * 当sysmo 服务器初始化完成后，必须调用此方法来将控制权交给sysmo 服务器。此方法
     * 中，sysmo服务器将等待客户端连接，并且处理已连接的客户端，或者与客户端相互传递
     * 消息。
     * <br />
     *
     * 当有已加入监控客户端连接时，此方法会实例化相应的{@link ConnectedNode}对象，
     * 并调用{@link ConnectedNode#connect()}方法将此客户端节点的后续动作交给此实
     * 例处理后继续监听新连接。
     * <br />
     *
     * 当有新的客户端节点连接时，此方法实例化相应的{@link NewNode}对象，并调用
     * {@link NewNode#waitConnect()}方法将此新客户端相应的控制权转交给此实例处理
     * 后继续监听新连接。
     * <br />
     *
     * 此方法在创建新的监听线程后将立即返回。{@link #shutdown()}方法会设置停止运行
     * 标志导致此方法创建的线程结束。具体请参看其说明。
     *
     * @see #SysmoServer(String)
     * @see ConnectedNode#connect()
     * @see NewNode#waitConnect()
     */
    public void doServerLoop() {
        isRunning = true;
        listeningThread = new Thread() {
            @Override
            public void run() {
                HashMap<String, String> map = ServerSettings.settings.settingMap;
                try {
                    while (isRunning) {
                        String signFile = map.get("/connection/sign-file");
                        if (!new File(signFile).exists()) {
                            signFile = new File(new File(configFile).getAbsoluteFile().getParent(), signFile).getAbsolutePath();
                        }
                        System.out.println("sysmo Server 正在监听端口：" + map.get("/connection/port"));
                        Socket sSocket = CommunicationInterface.
                                    waitForConnection(signFile,
                                map.get("/connection/password"),
                                Integer.parseInt(map.get("/connection/port")));
                        CommunicationInterface client =
                                new CommunicationInterface(sSocket);
                        initClient(client);
                    }
                } catch (Exception e) {
                    if (isRunning) {
                        e.printStackTrace();
                    }
                }
            }
        };
        listeningThread.start();
    }

    private synchronized void initClient(CommunicationInterface client) {
        try {
            Document doc = CommunicationProtocol.getDocumentFromString(
                    client.getString());
            int type = CommunicationProtocol.getType(doc);
            if (type != 0) { //客户端应该先发送类型为0的消息，否则断开与此客户端的连接。
                client.closeConnection();
                return;
            }
            org.w3c.dom.Node nodeIDNode =
                    doc.getElementsByTagName("clientID").item(0);
            if (nodeIDNode == null) {
                client.closeConnection();
                return;
            }
            int nodeID = Integer.parseInt(
                    nodeIDNode.getFirstChild().getNodeValue());
            AddedNode node = null;
            if (nodeID != 0) {
                node = addedNodes.get(nodeID);
                if (node == null || !(node instanceof UnconnectedNode)) {
                    client.sendString(CommunicationProtocol.encapsulate(4, ""));
                }
            }
            if (node == null) { //新客户端或非法的客户端ID
                NewNode nNode = new NewNode(dbInterface, this, client);
                newNodes.add(nNode);
                nNode.waitConnect();
                return;
            }
            //有效的客户端
            addedNodes.remove(nodeID);
            ConnectedNode cNode =
                    new ConnectedNode(this, dbInterface, client, nodeID);
            addedNodes.put(nodeID, cNode);
            cNode.connect();
        } catch (Exception e) {
            e.printStackTrace();
            client.closeConnection();
        }
    }

    /**
     * 关闭服务器。当sysmo 服务器端需要关闭时，此方法被调用。
     * <br />
     *
     * 此方法首先会使{@link #doServerLoop()}方法结束返回，使其不会产生新的连接会给
     * 所有已连接的和等待加入的客户端节点发送结束消息，等待所有连接断开后返回，从而可
     * 安全地停止并销毁服务器实例。
     *
     * <br />
     * 注意：客户端在得知服务器断开后仍会持续尝试连接，使得当服务器恢复运行后，其能够
     * 立即和服务器恢复通信，因此在断开客户端连接前必须先停止监听，否则可能导致已经断
     * 开的客户端重新连入服务器。
     */
    public void shutdown() {
        isRunning = false;
        CommunicationInterface.stopWaitConnection();
        /*for (NewNode newNode : newNodes) {
            newNode.stopWaitConnect();
        }*/
        while (newNodes.size() > 0) {
            newNodes.get(0).stopWaitConnect();
        }
        Vector<ConnectedNode> stopNodes = new Vector<ConnectedNode>();
        for (AddedNode addedNode : addedNodes.values()) {
            if (addedNode instanceof ConnectedNode) {
                stopNodes.add((ConnectedNode)addedNode);
                //((ConnectedNode)addedNode).closeConnection();
            }
        }
        for (ConnectedNode connectedNode : stopNodes) {
            connectedNode.closeConnection(true);
        }
        /*for (AddedNode addedNode : addedNodes.values()) {
            if (addedNode instanceof ConnectedNode) {
                ((ConnectedNode)addedNode).closeConnection();
            }
        }*/
    }

    /**
     * 通过用户名获得用户对象实例。此用户应已存在（已在服务器上注册）。
     * <br />
     *
     * 服务器主模块只提供通过用户名获得一个实例方法，其他所有的对于此用户或其他注册用
     * 户的操作都可通过此用户的{@link User}对象实例进行。
     *
     * <br />
     * 此类实例中并不保存所有用户列表，而是每次需要得到一个用户实例时都重新创建一个新
     * 实例，在一个用户的一次登录会话中，由外部的控制逻辑来保存获得的实例。由于用户登
     * 录频率不高，这样的做法不会过多占用服务器内存，也不至于明显影响性能。
     *
     * @param name 用户名。
     * @return 对应用户名的对象实例。
     * @throws UserNotExistsException 用户名不存在时抛出此异常。
     * @see User
     */
    public User getUser(String name) throws UserNotExistsException {
        return new User(dbInterface, name);
    }

    /**
     * 通过用户ID获取用户对象。
     *
     * @param id 用户ID。
     * @return 用户对象。
     * @throws UserNotExistsException 用户ID不存在。
     * @see #getUser(java.lang.String)
     */
    public User getUser(int id) throws UserNotExistsException {
        return new User(dbInterface, id);
    }

    /**
     * 根据节点ID得到某个节点实例。此方法首先在已连接列表中查找相应的节点，若已连接则
     * 返回{@link ConnectedNode}对象实例，否则返回{@link UnconnectedNode}对象实
     * 例。
     *
     * @param nodeID 节点ID。
     * @return 此节点实例。
     * @throws NodeNotExistsException 若节点ID不存在，则抛出此异常。
     */
    public synchronized AddedNode getNode(int nodeID) throws NodeNotExistsException {
        AddedNode node = addedNodes.get(nodeID);
        if (node == null) {
            throw new NodeNotExistsException(nodeID);
        }
        return node;
    }

    /**
     * 返回监控列表中的所有节点。
     * <br />
     *
     * 注意此列表内包含监控列表内记录的所有记录，包括已连接的和未连接的节点。此方法返
     * 回的{@link AddedNode}中若已连接，则实际为{@link ConnectedNode}，否则为
     * {@link UnconnectedNode}。
     *
     * @return 所有节点列表。
     * @see AddedNode#isConnected()
     */
    public synchronized List<AddedNode> getAllNodes() {
        Vector<AddedNode> allNodes = new Vector<AddedNode>();
        allNodes.addAll(addedNodes.values());
        return allNodes;
    }
    
    /**
     * 返回所有等待加入监控列表的新节点。
     *
     * @return 所有新节点列表。
     */
    public List<NewNode> getAllNewNodes() {
        return newNodes;
    }

    /**
     * 获取指定ID的警告对象。
     *
     * @param wid 警告ID。
     * @return 警告对象。
     * @throws WarningNotExistsException 警告ID不存在。
     */
    public Warning getWarning(int wid) throws WarningNotExistsException {
        DbWarning warning;
        try {
            warning = dbInterface.query_WarningTable(wid);
            if (warning == null) {
                throw new WarningNotExistsException(wid);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return new Warning(dbInterface, warning);
    }

    /**
     * 获得所有警报列表。
     *
     * @return 所有警告列表。
     */
    public List<Warning> getAllWarnings() {
        //TODO: 查询警告的默认无关ID
        Vector<DbWarning> warnings;
        try {
            warnings = dbInterface.query_WarningTable(0, 0, 0, 0);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        Vector<Warning> allWarnings = new Vector<Warning>();
        for (DbWarning warning : warnings) {
            allWarnings.add(new Warning(dbInterface, warning));
        }
        return allWarnings;
    }


    /**
     * 返回指定的监控项。此处得到的监控项根据此监控项当前所属的客户端节点而有所不同。
     * {@link ConnectedNode}类会生成能够进行在线配置的Item，因为这些设置会直接发送
     * 给客户端进行配置。而{@link UnconnectedNode}生成的Item不支持在线配置。
     *
     * @param id 监控项ID。
     * @return 指定的监控项。
     * @throws ItemNotExistsException 监控项ID不存在。
     */
    public Item getItem(int id) throws ItemNotExistsException {
        try {
            DbItem dbItem = dbInterface.query_ItemTable(id);
            if (dbItem == null) {
                throw new ItemNotExistsException(id);
            }
            AddedNode node = getNode(dbItem.cid);
            if (node.isConnected()) {
                return new ConnectedItem(dbInterface,
                        ((ConnectedNode)node).getCommunicationInterface(), id);
            } else {
                return new Item(dbInterface, id);
            }
        } catch (NodeNotExistsException e) {
            e.printStackTrace();
            return null;
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取指定的图表。
     *
     * @param id 图表ID。
     * @return 指定的图表。
     * @throws GraphGroupNotExistsException 图表ID不存在。
     */
    public GraphGroup getGraphGroup(int id)
            throws GraphGroupNotExistsException {
        return new GraphGroup(dbInterface, id);
    }

    /**
     * 获取指定的监控值。
     *
     * @param id 监控值ID。
     * @return 指定的监控值。
     * @throws ValueNotExistsException 监控值ID不存在。
     */
    public Value getValue(int id) throws ValueNotExistsException {
        return new Value(dbInterface, id);
    }

    /*
     * 以下两个方法为NodeStateChangeListener中要求实现的方法。当有节点被添加或删除
     * 时这两个方法会被调用，sysmo 服务器核心即可将相应的节点在等待添加列表和监控列
     * 表之间迁移或删除。
     */
    public synchronized void nodeAdded(NewNode oldNode, ConnectedNode newNode) {
        newNodes.remove(oldNode);
        addedNodes.put(newNode.getID(), newNode);
    }

    public synchronized void nodeRemoved(AddedNode node) {
        if (node instanceof ConnectedNode) {
            CommunicationInterface client =
                    ((ConnectedNode)node).getCommunicationInterface();
            if (client != null) {
                NewNode nNode = new NewNode(dbInterface, this, client);
                newNodes.add(nNode);
                nNode.waitConnect();
            }
        }
        addedNodes.remove(node.getID());
    }

    public synchronized void connectedNodeDisconnected(ConnectedNode node,
            int mode) {
        if (mode != CLIENT_SHUTDOWN) {
            System.out.println("与客户端连接异常中断(" + node.getName() + ")");
        }
        addedNodes.remove(node.getID());
        try {
            addedNodes.put(node.getID(), new UnconnectedNode(this, dbInterface,
                    node.getID()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void newNodeDisconnected(NewNode node, int mode) {
        if (mode != CLIENT_SHUTDOWN) {
            System.out.println("与新客户端连接异常中断");
        }
        newNodes.remove(node);
    }
}
